package jadeCW;

import jade.core.AID;
import jade.core.Agent;
import jade.core.behaviours.Behaviour;
import jade.core.behaviours.CyclicBehaviour;
import jade.domain.DFService;
import jade.domain.FIPAException;
import jade.domain.FIPANames;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.SearchConstraints;
import jade.domain.FIPAAgentManagement.ServiceDescription;
import jade.lang.acl.ACLMessage;
import jade.lang.acl.MessageTemplate;
import jade.proto.AchieveREInitiator;
import jade.proto.SubscriptionInitiator;
import jade.util.leap.Iterator;

import java.util.Date;
import java.util.HashMap;
import java.util.StringTokenizer;
import java.util.Vector;

public class PatientAgent extends Agent {
	// agent-wide shared data
	private Vector<Vector<String>> preferences = new Vector<Vector<String>>();
	int preferenceLevels = 1;
	int currentPreferenceLevel = 0;
	String appointmentNo;
	private AID hospitalAgent;
	private String targetAppointment;
	private AID targetAppointmentOwner;
	// booleans for synchronising behaviours
	private boolean hasAppointment = false;
	private boolean foundHospitalAgent = false;
	private boolean proposingSwap = false;
	
	protected void setup() {
		// process arguments
		Object[] args = getArguments();
	  	if (args != null && args.length > 0) {
	  		preferences.add(new Vector<String>());
	  		for(int i=0; i<args.length; i++) {
	  			String arg = (String)args[i];
	  			if(arg.equals("-")) {
	  				preferenceLevels++;
	  				preferences.add(new Vector<String>());
	  			} else {
	  				preferences.get(preferenceLevels-1).add(arg);
	  			}
	  		}
	  	}
	  	// print preference levels
	  	System.out.println(getLocalName()+ "'s preferences are: ");
	  	if(preferenceLevels==0) System.out.println("indifferent");
	  	for(int i=1; i<preferenceLevels; i++) {
	  		System.out.print("Preference Level "+i+": ");
	  		for(int j=0; j<preferences.get(i-1).size();j++) {
	  			System.out.print(preferences.get(i-1).get(j)+" ");
	  		}
	  		System.out.println();
	  	}
	  	
	 // Build the description used as template for the search
  		DFAgentDescription template = new DFAgentDescription();
  		ServiceDescription templateSd = new ServiceDescription();
  		templateSd.setType("allocate-appointments");
  		template.addServices(templateSd);
  		SearchConstraints sc = new SearchConstraints();
  		// We want to receive 1 result at most
  		sc.setMaxResults(new Long(1));
  		
  		//From DFSubscribeAgent in yellowPages
  		addBehaviour(new SubscriptionInitiator(this,DFService.createSubscriptionMessage(this, getDefaultDF(), template, sc)){
  			protected void handleInform(ACLMessage inform) {
  				try{
  					DFAgentDescription[] results = DFService.decodeNotification(inform.getContent());
  					if (results.length > 0) {
						DFAgentDescription dfd = results[0];
  		  				AID provider = dfd.getName();
  		  				// The same agent may provide several services; we are only interested
  		  				// in the allocate-appointments one
  		  				Iterator it = dfd.getAllServices();
  		  				while (it.hasNext()) {
  		  					ServiceDescription sd = (ServiceDescription) it.next();
  		  					if (sd.getType().equals("allocate-appointments")) {
  		  						System.out.println(getLocalName()+": Found service \""+sd.getName()+"\" of type \"allocate-appointments\" provided by agent "+provider.getLocalName());
  		  						hospitalAgent = provider;
  		  						foundHospitalAgent = true;
  		  					}
  		  				}
  					}
  				}
  				catch (FIPAException fe) {
  			  		fe.printStackTrace();
  			  	}
  			}
  		});
  		// add behaviours
	  	addBehaviour(new RequestAppointment());
	  	addBehaviour(new FindAppointmentOwner());
	  	addBehaviour(new ProposeSwap());
	  	addBehaviour(new RespondToProposal1());
	}
	
	// Put agent clean-up operations here
	  protected void takeDown() {
  		System.out.print(getLocalName()+": Appointment ");
  		if(hasAppointment == true) {
  			System.out.println(appointmentNo);
  		} else {
  			System.out.println("null");
  		}
	  }
	  
	  // function for finding the preference level of an appointment
	  private int findPreferenceLevel(String appointment) {
		  for(int i=0; i<preferenceLevels-1; i++) {
			  for(int j=0; j<preferences.get(i).size();j++) {
				  if(preferences.get(i).get(j).equals(appointment)) {
					  return i;
				  }
			  }
		  }
		  return preferenceLevels-1;
	  }
	  
	  // returns a vector of all more preferred appointments, ordered with most preferred first
	  private Vector<String> getPreferableAppointments() {
		  Vector<String> returnVector = new Vector<String>();
		  for(int i=0; i<currentPreferenceLevel; i++) {
			  for(int j=0; j<preferences.get(i).size();j++) {
				  returnVector.add(preferences.get(i).get(j));
			  }
		  }
		  return returnVector;
	  }
	
	  private class RequestAppointment extends Behaviour {
		private MessageTemplate mt;
		private int step = 0;
		@Override
		public void action() {
			switch(step){
			case 0:		// requesting appointments
				if(foundHospitalAgent && !hasAppointment){ 
					//Now ask the found agent for an appointment
			  		ACLMessage msg = new ACLMessage(ACLMessage.REQUEST);
			  		msg.addReceiver(hospitalAgent);
					msg.setConversationId("allocate-appointments");
					msg.setReplyWith("appointment"+System.currentTimeMillis());
					System.out.println(getLocalName()+": Sending REQUEST to "+hospitalAgent.getLocalName()+". Content is: "+msg.getContent());
					myAgent.send(msg);
					//Prepare the template to get replies
					mt = MessageTemplate.and(MessageTemplate.MatchConversationId("allocate-appointments"), 
							MessageTemplate.MatchInReplyTo(msg.getReplyWith()));
					step = 1;
				}
				break;
			case 1:		// receiving and processing responses
				ACLMessage reply = myAgent.receive(mt);
				if(reply != null) {
					if (reply.getPerformative()==ACLMessage.AGREE){
						appointmentNo = reply.getContent();
						currentPreferenceLevel = findPreferenceLevel(appointmentNo);
						hasAppointment = true;
						System.out.println(getLocalName() +": got appointment "+ appointmentNo);
					}
					else if (reply.getPerformative()==ACLMessage.REFUSE){
						System.out.println(getLocalName() +": got REFUSE, wait 2 seconds ");
						block(2000);
						step = 0;
					}
				}else {
					block();
				}
				break;
			}	
		}	

		@Override
		public boolean done() {
			return hasAppointment;
		}
	}
	
	  private class FindAppointmentOwner extends Behaviour {
		private MessageTemplate mt;
		private Vector<String> preferableAppointments;
		private String appointmentWhenFoundPreferable = null;
		private int step = 0;
		@Override
		public void action() {		
			if(hasAppointment && !proposingSwap) {	// don't search for a new appointment owner until current proposal is finished
				switch(step){
				case 0:		// find the target appointment	
						if ((preferableAppointments!=null) && appointmentWhenFoundPreferable.equalsIgnoreCase(appointmentNo)) {
							if(preferableAppointments.size()>0) {
								//Select an appointment
								targetAppointment = preferableAppointments.get(0);		
								//System.out.println(getLocalName()+": TargetAppointment is "+targetAppointment);
								step = 1;
							} else {
								//All of the more preferable appointments do not exist!
								block();
							}
						} else {							
							// find the more preferable appointments
							appointmentWhenFoundPreferable = appointmentNo;
							preferableAppointments = getPreferableAppointments();
						}
						break;
				case 1: // ask hospital for the owner of the target appointment
					block(10);	//Short block to allow other patients to receive appointments
			  		ACLMessage msg = new ACLMessage(ACLMessage.REQUEST);
			  		msg.addReceiver(hospitalAgent);
					msg.setConversationId("owner-AID");
					msg.setContent(targetAppointment);
					msg.setReplyWith("owner"+System.currentTimeMillis());
					System.out.println(getLocalName()+": Sending REQUEST to "+hospitalAgent.getLocalName()+". Content is: "+msg.getContent());
					myAgent.send(msg);
					//Prepare the template to get replies
					mt = MessageTemplate.and(MessageTemplate.MatchConversationId("owner-AID"), 
							MessageTemplate.MatchInReplyTo(msg.getReplyWith()));
					step = 2;
					break;
				case 2:		// receiving and processing responses
					ACLMessage reply = myAgent.receive(mt);
					if(reply != null) {
						if (reply.getPerformative()==ACLMessage.INFORM){
							targetAppointmentOwner = new AID(reply.getContent(), true);
							if ((targetAppointmentOwner.getLocalName().contains("patient")) ||
									(targetAppointmentOwner.getName().contains(hospitalAgent.getName()))) {
								//System.out.println(getLocalName() +": found "+targetAppointmentOwner.getLocalName()+" owns appointment "+targetAppointment);
								// add this appointment to back of list of things to try next
								preferableAppointments.remove(0);
								preferableAppointments.add(targetAppointment);
								step = 0;
								// pass to ProposeSwap behaviour
								proposingSwap = true;
							}
						}
						else if (reply.getPerformative()==ACLMessage.UNKNOWN){
							//the appointment does not exist, so remove it permanently
							preferableAppointments.remove(0);
							System.out.println(getLocalName()+": received UNKNOWN, appointment "+targetAppointment+" does not exist");
							step = 0;
						}
					} else {
						block();
					}
					break;
				}
			}
		}	
		
		@Override
		public boolean done() {
			return (currentPreferenceLevel==0)&&(hasAppointment);
		}
	}

	  private class ProposeSwap extends Behaviour{
		  private int step = 0;
		  private MessageTemplate mt;
		  private HashMap<AID, Vector<String>> previousProposals = new HashMap<AID, Vector<String>>();
		@Override
		public void action() {
			if(proposingSwap && (targetAppointmentOwner!=null)){
				switch(step){
				case 0:		// sending proposal
					if(appointmentNo.equals(targetAppointment)){
						//System.out.println(myAgent.getLocalName() + ": already has its preference, no need to make a proposal");
						proposingSwap = false;
						break;
					}
					if(previousProposals.get(targetAppointmentOwner)==null) {
						previousProposals.put(targetAppointmentOwner, new Vector<String>());
					}
					if(previousProposals.get(targetAppointmentOwner).contains(targetAppointment)){
						// do not send the same proposal more than once
						//System.out.println(myAgent.getLocalName() + ": Already asked "+targetAppointmentOwner.getLocalName()+", not asking again");
						proposingSwap = false;
						break;
					}
					//Propose to the agent with the preferred appointment for a swap
					ACLMessage msg = new ACLMessage(ACLMessage.PROPOSE);
					msg.addReceiver(targetAppointmentOwner);
					msg.setConversationId("swap-proposal");
					msg.setContent(appointmentNo + " " + targetAppointment);
					myAgent.send(msg);
					System.out.println(getLocalName()+": Sending PROPOSE TO SWAP to "+targetAppointmentOwner.getLocalName()+". Content is: "+msg.getContent());
					mt = MessageTemplate.MatchConversationId("swap-proposal");
					step = 1;
					break;
				case 1:		//Receives and processes reply from other agent
					ACLMessage reply = myAgent.receive(mt);
					if(reply!=null){
						if(reply.getPerformative()==ACLMessage.ACCEPT_PROPOSAL){
							appointmentNo = targetAppointment;
							currentPreferenceLevel = findPreferenceLevel(appointmentNo);
							previousProposals.clear(); //new appointmentNo, so can propose to anyone again
							System.out.println(getLocalName()+": successful proposal  - got appointment "+appointmentNo);
							//Sends the message to the hospital when the swap was successful between patients
							if(targetAppointmentOwner.getLocalName().contains("patient")){
								ACLMessage notifyHospital = new ACLMessage(ACLMessage.INFORM);
								notifyHospital.addReceiver(hospitalAgent);
								notifyHospital.setConversationId("swap-appointments");
								notifyHospital.setContent(appointmentNo);
								myAgent.send(notifyHospital);
								System.out.println("Hospital notified that "+ getLocalName() + " now has appointment "+appointmentNo);
							}
						}else if(reply.getPerformative()==ACLMessage.REJECT_PROPOSAL){
							System.out.println("Proposal to swap was rejected from "+ reply.getSender().getLocalName());
							// remember this and don't make the same proposal again
							previousProposals.get(targetAppointmentOwner).add(targetAppointment);
						}
						proposingSwap = false;
						step = 0;
					}
					else{
						block();
					}
					break;
				}
			}
		}

		@Override
		public boolean done() {
			return ((currentPreferenceLevel==0)&&hasAppointment);
		}
		  
	  }

	  private class RespondToProposal1 extends CyclicBehaviour{
		  private int prefLevelOfProposedAppointment = 0;
		@Override
		public void action() {
			// create message template
			MessageTemplate msg = MessageTemplate.and(MessageTemplate.MatchConversationId("swap-proposal"),
			MessageTemplate.MatchPerformative(ACLMessage.PROPOSE));
			ACLMessage proposal = myAgent.receive(msg);
			
			if(proposal!=null) { // wait for a proposal
				//Parse the content of the received message into two parts
				StringTokenizer st = new StringTokenizer(proposal.getContent());
				String proposedAppointment = st.nextToken();
				String wantedAppointment = st.nextToken();
				ACLMessage reply = proposal.createReply();
				if(!wantedAppointment.equals(appointmentNo)){
					//Reject straight away if it doesn't own the appointment, could happen if he had already swapped it with someone else
					reply.setPerformative(ACLMessage.REJECT_PROPOSAL);
					reply.setContent(myAgent.getLocalName()+" no longer owns appointment "+wantedAppointment);
					System.out.println(getLocalName()+": Proposal rejected since not owned");
					myAgent.send(reply);
				} else {
					//Currently holds the wanted appointment so checks the preference level of the proposed appointment
					prefLevelOfProposedAppointment = findPreferenceLevel(proposedAppointment);
					if(prefLevelOfProposedAppointment<=currentPreferenceLevel){
						//If the appointment to be received is at least as preferred as the one to be given away
						reply.setPerformative(ACLMessage.ACCEPT_PROPOSAL);
						appointmentNo = proposedAppointment;
						currentPreferenceLevel = findPreferenceLevel(appointmentNo);
						System.out.println(getLocalName()+": Proposal accepted from "+proposal.getSender().getLocalName()+" - new appointment is "+appointmentNo);
						myAgent.send(reply);
						//Sends the message to the hospital when the swap was successful between agents
						ACLMessage notifyHospital = new ACLMessage(ACLMessage.INFORM);
						notifyHospital.addReceiver(hospitalAgent);
						notifyHospital.setConversationId("swap-appointments");
						notifyHospital.setContent(appointmentNo);
						myAgent.send(notifyHospital);
						System.out.println(getLocalName()+": "+hospitalAgent.getLocalName()+" notified that new appointment is "+appointmentNo);
					}else{
						//If the one it currently has is preferred to the one being given away
						reply.setPerformative(ACLMessage.REJECT_PROPOSAL);
						reply.setContent("The proposed appointment is not preferred");
						System.out.println(getLocalName()+": Proposal rejected since not preferred");
						myAgent.send(reply);
					}
				}
			}
		}  
	}
}
